Skip to content

Avoid bun memory leak bug from TransformStream#4255

Open
TheodoreSpeaks wants to merge 4 commits intostagingfrom
fix/memory-leak
Open

Avoid bun memory leak bug from TransformStream#4255
TheodoreSpeaks wants to merge 4 commits intostagingfrom
fix/memory-leak

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

@TheodoreSpeaks TheodoreSpeaks commented Apr 22, 2026

Summary

Bun has a bug in TransformStream: oven-sh/bun#28035 causing high memory consumption due to no backpressure signal. We use this in

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • Validated that streaming responses still work fine with an agent block in a workflow locally.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 25, 2026 0:14am

Request Review

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@BugBot review

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 22, 2026

PR Summary

Medium Risk
Changes the executor’s streaming consumption/persistence path and adds new callback semantics; mistakes could truncate streamed outputs or skip memory persistence for agent runs. Scope is limited to streaming execution handling but affects a core runtime path.

Overview
Redesigns streaming handling to avoid Bun’s TransformStream memory/backpressure bug. BlockExecutor.handleStreamingExecution no longer tee()s and separately drains an executor stream; it now reads from a single ReadableStreamDefaultReader, forwards bytes to the client via a new ReadableStream, and accumulates decoded text only when the source fully drains.

Adds a new StreamingExecution.onFullContent hook and moves agent memory persistence to it. Agent streaming no longer wraps the source stream with a persistence TransformStream; instead it registers an onFullContent callback to append the final assistant message to memory after the stream completes. Streaming output assembly also now guards against persisting/trusting truncated content and logs when the client consumer exits early.

Reviewed by Cursor Bugbot for commit c6272d9. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/executor/execution/block-executor.ts
Previously, if the onStream consumer caught an internal error without
re-throwing, the block-executor would treat the shortened accumulator
as the complete response, persist a truncated string to memory via
appendToMemory, and set it as executionOutput.content.

Track whether the source ReadableStream actually closed (done=true) in
the pull handler. If onStream returns before the source drains, skip
content persistence and log a warning — the old tee()-based flow was
immune to this because the executor branch drained independently of
the client branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@BugBot review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit bf2f9af. Configure here.

@TheodoreSpeaks TheodoreSpeaks marked this pull request as ready for review April 25, 2026 00:09
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

This PR fixes a Bun runtime memory leak (oven-sh/bun#28035) caused by TransformStream lacking backpressure signals. The fix removes wrapStreamForPersistence (which piped through a TransformStream) and replaces it with a pull-based ReadableStream that accumulates chunks inline, then delivers the assembled content via a new onFullContent callback on StreamingExecution after the stream fully drains.

Confidence Score: 5/5

Safe to merge; the fix correctly avoids the Bun TransformStream memory leak and all remaining findings are P2 style suggestions.

The core logic is sound: the pull-based approach eliminates the TransformStream, backpressure is correctly propagated, and edge cases (stream error, early consumer exit, empty content) are all guarded. The only finding is a minor inconsistency where onFullContent is called even when fullContent is empty, which is handled defensively by the agent handler today.

apps/sim/executor/execution/block-executor.ts — minor onFullContent guard inconsistency at line 718.

Important Files Changed

Filename Overview
apps/sim/executor/execution/block-executor.ts Replaces tee()-based dual-stream approach with a pull-based ReadableStream that accumulates chunks inline; small inconsistency in onFullContent guard logic.
apps/sim/executor/handlers/agent/memory.ts Removes wrapStreamForPersistence (the TransformStream that triggered the Bun memory leak); appendToMemory is now called directly via onFullContent.
apps/sim/executor/handlers/agent/agent-handler.ts Wires onFullContent callback on the StreamingExecution object instead of wrapping the stream in a TransformStream.
apps/sim/executor/types.ts Adds optional onFullContent callback to StreamingExecution interface with a clear JSDoc explaining the Bun bug motivation.

Sequence Diagram

sequenceDiagram
    participant AH as AgentHandler
    participant BE as BlockExecutor
    participant CS as clientSource (ReadableStream)
    participant PC as processedClientStream
    participant OS as onStream consumer

    AH->>BE: StreamingExecution { stream, execution, onFullContent }
    BE->>CS: Create pull-based ReadableStream (reads from sourceReader, accumulates chunks)
    BE->>PC: streamingResponseFormatProcessor.processStream(clientSource)
    BE->>OS: ctx.onStream({ stream: processedClientStream, execution })
    loop For each chunk
        OS->>PC: read chunk
        PC->>CS: pull
        CS->>CS: sourceReader.read() → decode → accumulated.push()
        CS-->>PC: enqueue(value)
        PC-->>OS: chunk
    end
    OS-->>BE: stream fully drained (sourceFullyDrained = true)
    BE->>BE: executionOutput.content = accumulated.join('')
    BE->>AH: onFullContent(fullContent)
    AH->>AH: memoryService.appendToMemory(ctx, inputs, { role: 'assistant', content })
Loading

Reviews (1): Last reviewed commit: "fix lint" | Re-trigger Greptile

Comment on lines +718 to +724
if (streamingExec.onFullContent) {
try {
await streamingExec.onFullContent(fullContent)
} catch (error) {
this.execLogger.error('onFullContent callback failed', { blockId, error })
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 onFullContent called unconditionally with empty string

onFullContent is invoked regardless of whether fullContent is empty, while the preceding block that sets executionOutput.content is guarded by if (fullContent). The current agent handler defensively checks !content.trim(), but any future onFullContent implementor might not, leading to spurious empty-content persistence calls.

Suggested change
if (streamingExec.onFullContent) {
try {
await streamingExec.onFullContent(fullContent)
} catch (error) {
this.execLogger.error('onFullContent callback failed', { blockId, error })
}
}
if (streamingExec.onFullContent && fullContent) {
try {
await streamingExec.onFullContent(fullContent)
} catch (error) {
this.execLogger.error('onFullContent callback failed', { blockId, error })
}
}

…ntent symmetric

Previously, executionOutput.content was guarded by `if (fullContent)`
but `onFullContent` fired regardless. The agent-handler implementor
defensively bails on empty/whitespace content, but that's a callee
contract, not enforced at the call site — future implementors could
spuriously persist empty assistant turns to memory.

Hoist the `!fullContent` check to a single early return, so both the
output write and the callback share the same precondition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant